/*
 * 
 * Copyright (C) 2010 Wilfried Elmenreich, Anita Sobe 
 * This file is part of VideoNetwork.
 * 
 * VideoNetwork is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. 
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 
 * You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>. 
 */
package consumer;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Random;

import proxy.Content;
import proxy.ServerNode;
import proxy.UnitContainer;
import proxy.VideoUnit;
import simulation.Simulator;
import simulation.SimulatorConfig.ScenarioType;
import simulation.StatKeeper;

/**
 * The Consumer class stores references to the videos currently viewed and
 * calculates the next update instant (when a video is over)
 * 
 */
public abstract class Consumer {

	//
	protected Random rand;
	// consumer id
	private int id;

	/**
	 * pMindChange is the probability that a consumer adjusts her taste when
	 * viewing a video
	 */
	private double pMindChange = 0.1;

	protected static final int noViewings = 10;

	protected int maxHopDistance = 10;

	protected static final double minDeadline = 0.04;

	// overriden by other consumers
	protected int noFavorites;

	protected LinkedList<VideoUnit> favoriteList = new LinkedList<VideoUnit>();

	/**
	 * sum of distances in favorite list, used to determine next preference
	 * based on cumulative probability
	 */
	protected double sumDistances = 0.0;
	protected double interestRadius = 0.0;
	//
	protected int[] likesContent;

	// history
	protected ArrayList<Viewing> viewings = new ArrayList<Viewing>();
	// only one unit at a time
	protected Viewing currentlyViewing = null;

	protected Simulator simulator;
	protected ServerNode server;

	protected double defaultDatarate = 0;

	protected int requestSize = 0;
	protected int missed = 0;

	private RequestGenerator reqGenerator;

	private StatKeeper interRequestDelay = new StatKeeper();

	/**
	 * 
	 * @param simulator
	 * @param server
	 */
	public Consumer(int id, Simulator simulator, ServerNode server, Random rand) {
		this.id = id;
		this.simulator = simulator;
		this.server = server;
		this.defaultDatarate = server.getLinks().get(0).getDatarate();
		this.rand = rand;
		initLikesContent();
		// define which requests are generated
		if (simulator.getScenario() == ScenarioType.UNITSIZE
				|| simulator.getScenario() == ScenarioType.UNITSIZEREFSEARCH) {
			reqGenerator = new AutoDirectorRequest(this);
		} else {
			reqGenerator = new FixedSizeRequest(this);
		}
	}

	/**
	 * 
	 */
	protected void initLikesContent() {
		likesContent = new int[simulator.getContentTypes()];
		// take any out of the taglist
		Content[] tags = simulator.getTagDirectory().keySet()
				.toArray(new Content[0]);
		likesContent = Arrays.copyOf(
				tags[getRand().nextInt(tags.length - 1)].content,
				simulator.getContentTypes());

		// for (int i = 0; i < simulator.getContentTypes(); i++)
		// likesContent[i] = getRand().nextInt(100);
	}

	/**
	 * present all units at once (because of parallel specification)
	 */
	protected void presentMixedUnits(ArrayList<UnitContainer> ucs,
			ArrayList<Viewing> seq1) {

		double nextstarttimeseq1 = simulator.getSimTime();
		double nextstarttimeseq2 = simulator.getSimTime();

		for (Viewing vi : viewings) {

			UnitContainer uc = ucs.get(viewings.indexOf(vi));
			if (seq1.contains(vi)) {
				vi.setViewStartTime(nextstarttimeseq1);
				nextstarttimeseq1 = simulator.getSimTime()
						+ vi.getVideoUnit().playLength();
				vi.setViewEndTime(nextstarttimeseq1);
			} else {
				vi.setViewStartTime(nextstarttimeseq2);
				nextstarttimeseq2 = simulator.getSimTime()
						+ vi.getVideoUnit().playLength();
				vi.setViewEndTime(nextstarttimeseq2);
			}

			uc.viewedUntil = vi.getViewEndTime();
			// simulator.numHopsStat.add(uc.numHopsTraveled);
			// uc.numHopsTraveled = 0;
			if (uc.getTimeReceived() < vi.getRequestTime()) {
				simulator.delayStat.add(0);
				simulator.hitStat.add(0);
			} else {
				simulator.delayStat.add(uc.getTimeReceived()
						- vi.getRequestTime());
				simulator.hitStat.add(vi.getViewStartTime()
						- vi.getRequestTime());
			}
			double diff = vi.getDeadline() - uc.getTimeReceived();
			simulator.deadlineStat.add(diff > 0 ? diff : 0);
			simulator.log("viewing u" + uc.getVideoUnit().getId() + " c: "
					+ uc.getVideoUnit().getContent() + " for "
					+ vi.getVideoUnit().playLength());
		}
	}

	/**
	 * present all units at once
	 * 
	 * @param ucs
	 */
	protected void presentParallelUnits(ArrayList<UnitContainer> ucs) {

		for (Viewing vi : viewings) {
			UnitContainer uc = ucs.get(viewings.indexOf(vi));
			vi.setViewStartTime(simulator.getSimTime());
			vi.setViewEndTime(simulator.getSimTime()
					+ vi.getVideoUnit().playLength());
			uc.viewedUntil = vi.getViewEndTime();
			// simulator.numHopsStat.add(uc.numHopsTraveled);
			// uc.numHopsTraveled = 0;
			if (uc.getTimeReceived() < vi.getRequestTime()) {
				simulator.delayStat.add(0);
				simulator.hitStat.add(0);
			} else {
				simulator.delayStat.add(uc.getTimeReceived()
						- vi.getRequestTime());
				simulator.hitStat.add(vi.getViewStartTime()
						- vi.getRequestTime());
			}
			double diff = vi.getDeadline() - uc.getTimeReceived();
			simulator.deadlineStat.add(diff > 0 ? diff : 0);
			simulator.log("viewing u" + uc.getVideoUnit().getId() + " for "
					+ vi.getVideoUnit().playLength());
		}
	}

	/**
	 * simple scenario - units are presented immediately if available
	 */
	public void process() {
		boolean newRequest = false;
		// 1) fill up viewings
		if (viewings.size() == 0) {
			generateNewRequest();
			newRequest = true;
			server.countRequest(viewings);
			requestSize = viewings.size();
			missed = 0;
		}
		// 1a) request to be fulfilled
		ArrayList<Viewing> stillToDeliver = new ArrayList<Viewing>();
		Iterator<Viewing> it = null;
		for (it = viewings.iterator(); it.hasNext();) {
			Viewing vi = it.next();
			// 2) present local units to client and request others
			if (this.simulator.getSimTime() < vi.getViewStartTime()) {
				// present ALL local units fitting the request
				ArrayList<UnitContainer> result = this.server
						.searchPresentUnits(vi.getVideoUnit().getContent());

				if (result.size() > 0) {
					UnitContainer resU = result.get(0);
					presentSequentialLocalUnit(vi, resU);

				} else {
					// deadline missed - we don't request it anymore!
					if (checkDeadline(vi)) {
						it.remove();
						cleanupViewings();
					} else {
						stillToDeliver.add(vi);
					}
				}
			}
			// 3) cleanup presentations that are done
			else if (simulator.getSimTime() > vi.getViewEndTime()) {
				// possibly the user now adjusts her taste to what she has
				// just
				// seen
				if (getRand().nextDouble() < getpMindChange()) {
					for (int j = 0; j < likesContent.length; j++) {
						likesContent[j] = vi.getVideoUnit().getContent().content[j];
					}
					// favorite videos must be recalculated for user's new
					// taste
					chooseFavorites();
				}

				it.remove();
				// request is fulfilled if all units are delivered
				cleanupViewings();
			}
		}
		// all units that have still to be delivered
		server.increaseInterest(stillToDeliver, newRequest, false);
	}

	public void generateNewRequest() {
		reqGenerator.generateNewRequest();
	}

	protected void cleanupViewings() {
		if (viewings.size() == 0) {
			if (missed == 0) {
				simulator.requestFulfilledCounter++;
			} else if (missed < requestSize) {
				simulator.requestsPartiallyFulfilled++;
			} else {
				simulator.requestsNotFulfilledAtall++;
			}

			if (requestSize > 1) {
				simulator.interCompositionDelay.add(this.interRequestDelay
						.mean());
				simulator.interCompositionJitter
						.add(this.interRequestDelay.max);
				this.interRequestDelay.clear();
			}
		}
	}

	public abstract void processMixed();

	public abstract void processParallel();

	public abstract void processSequence();

	public abstract void chooseFavorites();

	protected void addUnitUtilizationStat() {

		for (Viewing v : viewings) {
			simulator.unitsCurrentlyInPresentation.add(1 / simulator
					.unitCount(v.getVideoUnit().getId()));
		}
	}

	/**
	 * present one unit after another
	 * 
	 * @param current
	 * @param uc
	 */
	protected void presentSequentialLocalUnit(Viewing current, UnitContainer uc) {

		current.setViewStartTime(simulator.getSimTime());
		current.setViewEndTime(simulator.getSimTime()
				+ current.getVideoUnit().playLength());

		uc.viewedUntil = current.getViewEndTime();
		// simulator.numHopsStat.add(uc.numHopsTraveled);
		// if it is in presentation, then numHopsTravelled has to be reset
		uc.numHopsTraveled = 0;
		double delay;
		// it was already present at request time
		if (uc.getTimeReceived() <= current.getRequestTime()) {
			delay = 0.0;
		} else {
			delay = uc.getTimeReceived() - current.getRequestTime();
		}
		if (viewings.size() < requestSize) {
			interRequestDelay.add(delay);
		}

		int position = requestSize - viewings.size();
		simulator.delayPerUnitwithinRequest.get(position).add(delay);

		simulator.delayStat.add(delay);
		simulator.hitStat.add(current.getViewStartTime()
				- current.getRequestTime());

		simulator.deadlineNotMissedCount++;

		simulator.deadlineStat.add(current.getDeadline()
				- current.getRequestTime());
		int unitid = current.getVideoUnit().getId();
		double currentCount = 0.0;
		if (simulator.unitUtilizationCount.containsKey(unitid)) {
			currentCount = simulator.unitUtilizationCount.get(unitid);
		}
		double newValue = currentCount + 1
				/ (double) simulator.unitCount(unitid);
		simulator.unitUtilizationCount.put(unitid, newValue > 1.0 ? 1.0
				: newValue);
		;
		simulator.log("c:" + id + " at S:" + server.getId() + " viewing u"
				+ uc.getVideoUnit().getId() + " for "
				+ current.getVideoUnit().playLength() + " startup delay: "
				+ (current.getViewStartTime() - current.getRequestTime()));

		currentlyViewing = current;
	}

	protected void handleSeqUnitPresentationDone() {

		viewings.remove(0);
		currentlyViewing = null;
		// interpacket delay (startup delay!)
		if (viewings.size() > 0) {
			Viewing next = viewings.get(0);
			next.setRequestTime(simulator.getSimTime());
			next.setDeadline(simulator.getSimTime() + minDeadline);

			// try to present next unit
			UnitContainer uc = server.searchPresentUnit(next.getVideoUnit()
					.getId());
			if (uc != null) {
				presentSequentialLocalUnit(next, uc);
			} else {
				// check deadline
				checkDeadline(next);
				server.increaseInterest(viewings, false, true);
			}
		} else {
			// request done
			cleanupViewings();
		}
	}

	public int getId() {
		return id;
	}

	public ServerNode getServer() {
		return server;
	}

	public ArrayList<Viewing> getViewings() {
		return viewings;
	}

	public LinkedList<VideoUnit> getFavoriteList() {
		return favoriteList;
	}

	public void setRand(Random rand2) {
		this.rand = rand2;
	}

	public void setpMindChange(double pMindChange) {
		this.pMindChange = pMindChange;
	}

	public double getpMindChange() {
		return pMindChange;
	}

	public Random getRand() {
		return rand;
	}

	public int[] getLikesContent() {
		return likesContent;
	}

	public Content getLikesContentAsContent() {
		return new Content(likesContent);
	}

	public void setLikesContent(int[] likesContent) {
		this.likesContent = likesContent;
	}

	// if the corresponding node was killed we still need the statistics
	public void kill() {
			for (Viewing vi : viewings) {
				simulator.deadlineMissedCount++;
				simulator.delayStat.add(vi.getDeadline() - vi.getRequestTime());
				simulator.hitStat.add(vi.getDeadline() - vi.getRequestTime());
			}
			if (this.viewings.size() == requestSize) {
				simulator.requestsNotFulfilledAtall++;
			} else {
				simulator.requestsPartiallyFulfilled++;
			}
	}

	protected boolean checkDeadline(Viewing current) {

		if (simulator.getSimTime() - current.getDeadline() > 0) {

			simulator
					.log("c" + getId() + ": at S:" + server.getId()
							+ " deadline missed for u"
							+ current.getVideoUnit().getId());

			simulator.deadlineMissedCount++;
			int position = requestSize - viewings.size();
			double delay = current.getDeadline() - current.getRequestTime();
			simulator.delayStat.add(delay);
			if (viewings.size() < requestSize) {
				this.interRequestDelay.add(delay);
			}
			simulator.delayPerUnitwithinRequest.get(position).add(delay);
			simulator.hitStat.add(delay);
			simulator.deadlineStat.add(delay);
			missed++;
			server.decreaseInterest(current);
			return true;
		}
		return false;
	}

	protected void tryToPresentNextUnit(Viewing next) {
		UnitContainer uc = server
				.searchPresentUnit(next.getVideoUnit().getId());
		if (uc != null) {
			presentSequentialLocalUnit(next, uc);
		}
	}

	public int maxUnitsPerRequest() {
		return reqGenerator.maxUnitsPerRequest();
	}

}

class AutoDirectorRequest implements RequestGenerator {

	// see Manfreds CHI Paper (LNF settings)
	// 30, 60, 90, 180
	private static final double minRequestLengthinSec = 30.0;

	private Consumer c;

	public AutoDirectorRequest(Consumer c) {
		this.c = c;
	}

	public void generateNewRequest() {

		c.simulator.requestSubmittedCounter++;

		double currentLength = 0.0;
		ArrayList<Content> request = new ArrayList<Content>();
		while (currentLength < minRequestLengthinSec) {

			double p = c.getRand().nextDouble() * c.sumDistances;
			double cumulation = 0.0;
			VideoUnit vu = null;

			for (int i = 0; i < c.favoriteList.size(); i++) {

				vu = c.favoriteList.get(i);
				cumulation += c.interestRadius - vu.getDistance();

				if (cumulation > p) {
					boolean alreadythere = false;
					for (Viewing v : c.viewings) {
						if (v.getVideoUnit().getId() == vu.getId()) {
							alreadythere = true;
							break;
						}
					}
					if (alreadythere) {
						continue;
					}

					Viewing vi = new Viewing(vu, c.simulator.getSimTime());

					currentLength += vu.playLength();

					// initialize request time and deadline for the units (may
					// be updated later)
					vi.setRequestTime(c.simulator.getSimTime());

					double deadline = c.simulator.getSimTime()
							+ c.maxHopDistance
							* (vi.getVideoUnit().getSizeInBytes() / c.defaultDatarate);
					if (deadline < c.simulator.getSimTime()
							+ Consumer.minDeadline)
						deadline = c.simulator.getSimTime()
								+ Consumer.minDeadline;

					vi.setDeadline(deadline);

					c.viewings.add(vi);
					request.add(vu.getContent());
				}
				c.simulator.writeTraceFile(c.getServer().getId(),
						request.toArray(new Content[request.size()]));
				// request done - outer loop has to be finished too
				if (currentLength > minRequestLengthinSec
						|| c.viewings.size() == c.favoriteList.size()) {
					c.missed = 0;
					c.requestSize = c.viewings.size();
					c.simulator.deadlineSetCount += c.requestSize;
					String msg = "";
					for (Viewing v : c.viewings) {
						msg += " u" + v.getVideoUnit().getId() + " "
								+ v.getVideoUnit().getContent() + " "
								+ v.getVideoUnit().getDistance();
					}
					c.simulator.log("C" + c.getId() + " S" + c.server.getId()
							+ " new req: " + msg);
					return;
				}
			}
		}

	}

	@Override
	public int maxUnitsPerRequest() {
		return (int) (Math.round(minRequestLengthinSec
				/ (VideoUnit.getMinSize() * 8 / VideoUnit.getVideoBitRate())) + 1);
	}
}

class FixedSizeRequest implements RequestGenerator {

	private Consumer c;

	public FixedSizeRequest(Consumer c) {
		this.c = c;
	}

	public void generateNewRequest() {
		c.simulator.requestSubmittedCounter++;
		// model different number of viewings (request)
		int localNoViewings = c.getRand().nextInt(Consumer.noViewings) + 1;
		Content[] request = new Content[localNoViewings];
		outer: while (c.viewings.size() < localNoViewings) {
			double p = c.getRand().nextDouble() * c.sumDistances;
			double cumulation = 0.0;
			VideoUnit vu = null;
			for (int i = 0; i < c.favoriteList.size()
					&& c.viewings.size() < localNoViewings; i++) {
				vu = c.favoriteList.get(i);
				cumulation += c.interestRadius - vu.getDistance();
				if (cumulation > p) {
					for (Viewing v : c.viewings) {
						if (v.getVideoUnit().getId() == vu.getId()) {
							continue outer;
						}
					}

					Viewing vi = new Viewing(vu, c.simulator.getSimTime());
					// initialize request time and deadline for the units (may
					// be updated later)
					vi.setRequestTime(c.simulator.getSimTime());

					double deadline = c.simulator.getSimTime()
							+ c.maxHopDistance
							* (vi.getVideoUnit().getSizeInBytes() / c.defaultDatarate);
					if (deadline < c.simulator.getSimTime()
							+ Consumer.minDeadline)
						deadline = c.simulator.getSimTime()
								+ Consumer.minDeadline;
					vi.setDeadline(deadline);
					c.simulator.deadlineSetCount++;

					c.viewings.add(vi);
					request[c.viewings.size() - 1] = vu.getContent();
				}
			}
		}
		c.simulator.writeTraceFile(c.getServer().getId(), request);
		c.missed = 0;
		c.requestSize = c.viewings.size();
		String msg = "";
		for (Viewing vi : c.viewings) {
			msg += " u" + vi.getVideoUnit().getId() + " "
					+ vi.getVideoUnit().getContent() + " "
					+ vi.getVideoUnit().getDistance();
		}
		c.simulator.log("C" + c.getId() + " S" + c.server.getId()
				+ " new req: " + msg);
	}

	@Override
	public int maxUnitsPerRequest() {
		return Consumer.noViewings;
	}

}

interface RequestGenerator {

	public void generateNewRequest();

	public int maxUnitsPerRequest();

}
